# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import logging
import typing
import urllib.parse

import requests
import tornado.gen
import tornado.ioloop
import tornado.httpclient

from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway_util import (
    PrivateExchangeMessage,
    convert_to_requests_response,
    schedule,
)


def _extract_url(exc):
  request = getattr(exc, 'request', None)
  if request is not None:
    parsed_result = urllib.parse.urlparse(request.url)
    url = urllib.parse.urlunparse(parsed_result[:3] + (None, None, None))
  else:
    url = 'No PrepareRequest object in the exception!'
  return url


def wrap_async_task(
    async_func,
    *,
    msg_type=None,
    publisher=None,
    run_once=False,
    func_params=None,
):
  func_params = func_params or {}

  @tornado.gen.coroutine
  def wrapper():
    logger = logging.getLogger(__name__)
    request_timestamp = get_timestamp()
    ret = schedule.CancelJob if run_once else None
    # noinspection PyBroadException
    try:
      response = yield async_func(**func_params)
      response = convert_to_requests_response(response)
    except requests.exceptions.ReadTimeout as e:
      logger.error('%s, %s', e, _extract_url(e))
      return ret
    except requests.exceptions.ConnectTimeout as e:
      logger.error('%s, %s', e, _extract_url(e))
      return ret
    except requests.exceptions.ConnectionError as e:
      logger.error('%s, %s', e, _extract_url(e))
      return ret
    except requests.exceptions.ChunkedEncodingError as e:
      logger.error('%s, %s', e, _extract_url(e))
      return ret
    except tornado.httpclient.HTTPClientError as e:
      response = e.response
      if response is None:
        logger.error('%s', e)
        return ret
      else:
        if not isinstance(response, tornado.httpclient.HTTPResponse):
          logger.warning('Weird response! %s', response)
          return ret

        if response.code == 599:
          logger.error('%s, Timeout/Connection Error!', e)
          return ret
        else:
          logger.error('%s, %s', e, response.code, response.effective_url)
          # No return here, pass down and continue
          response = convert_to_requests_response(response)
    except Exception:
      logger.exception('Unknown Exception')
      return ret

    if publisher is not None:
      assert msg_type is not None
      timestamp = get_timestamp()
      msg = PrivateExchangeMessage(
          msg_type=msg_type,
          data=response,
          received_timestamp=timestamp,
          request_params=func_params,
          request_timestamp=request_timestamp,
      )
      publisher.write(timestamp, msg)
    return ret

  return wrapper


class Scheduler(object):
  def __init__(self, ioloop=None, logger=None):
    self._scheduler = schedule.Scheduler()
    self._periodic_callback = None
    self._ioloop = ioloop or tornado.ioloop.IOLoop.current()
    self._logger = logger or logging.getLogger(__name__)

  def has_job_name(self, job_name):
    for job in self._scheduler.jobs:
      if job_name in job.tags:
        return True
    return False

  def add_periodic_task(self,
                        period,
                        job_func,
                        *,
                        job_name=None,
                        immediate_first_run=True,
                        **kwargs):
    assert period > 0, period

    if immediate_first_run:
      self._ioloop.add_callback(job_func, **kwargs)

    if job_name is None:
      self._scheduler.every(period).seconds.do(job_func, **kwargs)
    else:
      if self.has_job_name(job_name):
        self._logger.error('Job name conflict! %s. Add task failed!', job_name)
      else:
        self._scheduler.every(period).seconds.do(job_func, **kwargs).tag(job_name)

  def remove_periodic_task(self, job_name):
    job = None
    for job in self._scheduler.jobs:
      if job_name in job.tags:
        break
    if job is None:
      self._logger.error('job %s not found!', job_name)
    else:
      self._scheduler.cancel_job(job)

  def run_once(self, job_func, delay=0, **kwargs):
    self._ioloop.call_later(delay, job_func, **kwargs)

  def start(self, later_td: typing.Optional[float] = 0.0):
    if self._periodic_callback is not None and self._periodic_callback.is_running():
      self._logger.error('Scheduler is running already!')
      return

    self._periodic_callback = tornado.ioloop.PeriodicCallback(self._scheduler.run_pending, 500)
    self._ioloop.call_later(later_td, self._periodic_callback.start)

  def stop(self):
    if self._periodic_callback is not None:
      self._periodic_callback.stop()
      self._periodic_callback = None
    else:
      self._logger.error('Scheduler is stopped already!')

  def clear(self):
    self._scheduler.clear()
